---
-- 规则操作符

local config = require("config")

local _M = {}

local not_char = "!"

local ipmatcher
if (require("ffi")).os == "Windows" then
    ipmatcher = require("lib.packages.ipmatcher_win")
else
    ipmatcher = require("lib.packages.ipmatcher")
end

local maxminddb = require("lib.packages.maxminddb")
if not maxminddb.initted() then
    maxminddb.init(config.waf_base_path .. "/data/Country.mmdb")
end

---操作符是否以"!"开头
---     - true: 是"!"开头
---     - false: 不是"!"开头
---@param op string 操作符
---@return boolean
local function is_prefix_not(op)
    local from = string.find(op, not_char, 1, true)
    if from == 1 then
        return true
    end
    return false
end

local op_event = {
    _re = function(val_var, val_rule)
        local t = type(val_rule)
        if t == "table" then
            for _, v in ipairs(val_rule) do
                local from, _, _ = ngx.re.find(val_var, v, "ijos")
                if from then
                    return true
                end
            end
        elseif t == "string" then
            local from, _, _ = ngx.re.find(val_var, val_rule, "ijos")
            if from then
                return true
            end
        else
            return nil, '(op:re) rule-value accepted "string"/"string array" only.'
        end
        return false
    end,
    _eq = function(val_var, val_rule)
        if type(val_var) ~= "string"
            and type(val_var) ~= "number"
        then
            return nil, '(op:eq) var-value accepted "string"/"number" only.'
        end
        if type(val_rule) ~= "string"
            and type(val_rule) ~= "number"
        then
            return nil, '(op:eq) rule-value accepted "string"/"number" only.'
        end
        local val_var_num = tonumber(val_var)
        local val_rule_num = tonumber(val_rule)
        if (not val_var_num) or (not val_rule_num) then
            -- 转换数值失败,直接比较
            return (tostring(val_var) == tostring(val_rule))
        end
        return val_var_num == val_rule_num
    end,
    _lt = function(val_var, val_rule)
        if type(val_var) ~= "string"
            and type(val_var) ~= "number"
        then
            return nil, '(op:lt) var-value accepted "string"/"number" only.'
        end
        if type(val_rule) ~= "string"
            and type(val_rule) ~= "number"
        then
            return nil, '(op:lt) rule-value accepted "string"/"number" only.'
        end
        local val_var_num = tonumber(val_var)
        local val_rule_num = tonumber(val_rule)
        if (not val_var_num) or (not val_rule_num) then
            -- 转换数值失败,直接比较
            return (tostring(val_var) < tostring(val_rule))
        end
        return val_var_num < val_rule_num
    end,
    _gt = function(val_var, val_rule)
        if type(val_var) ~= "string"
            and type(val_var) ~= "number"
        then
            return nil, '(op:gt) var-value accepted "string"/"number" only.'
        end
        if type(val_rule) ~= "string"
            and type(val_rule) ~= "number"
        then
            return nil, '(op:gt) rule-value accepted "string"/"number" only.'
        end
        local val_var_num = tonumber(val_var)
        local val_rule_num = tonumber(val_rule)
        if (not val_var_num) or (not val_rule_num) then
            -- 转换数值失败,直接比较
            return (tostring(val_var) > tostring(val_rule))
        end
        return val_var_num > val_rule_num
    end,
    _startwith = function(val_var, val_rule)
        if type(val_var) ~= "string" or type(val_rule) ~= "string" then
            return nil, '(op:startwith) var-value and rule-value accepted "string" only.'
        end
        -- local from, _ = string.find(val_var, val_rule, 1, true)
        local from, _ = string.find(string.lower(val_var), val_rule, 1, true)
        return from == 1
    end,
    _endwith = function(val_var, val_rule)
        if type(val_var) ~= "string" or type(val_rule) ~= "string" then
            return nil, '(op:endwith) var-value and rule-value accepted "string" only.'
        end
        local from, _ = string.find(string.lower(val_var), val_rule, -1 * string.len(val_rule), true)
        return from ~= nil
    end,
    _contain = function(val_var, val_rule)
        if type(val_var) ~= "string" or type(val_rule) ~= "string" then
            return nil, '(op:contain) var-value and rule-value accepted "string" only.'
        end
        local from, _ = string.find(string.lower(val_var), val_rule, 1, true)
        return from ~= nil
    end,
    _in = function(val_var, val_rule)
        if type(val_rule) ~= "table" then
            return nil, '(op:in) rule-value accepted "array" only.'
        end
        local value = string.lower(tostring(val_var))
        for _, v in ipairs(val_rule) do
            if value == string.lower(tostring(v)) then
                return true
            end
        end
        return false
    end,
    ---运算ip_match
    ---@param ip string
    ---@param val_rule table | string
    ---@return boolean | nil
    _ip_match = function(ip, val_rule)
        local final_value
        if type(val_rule) == "table" then
            final_value = val_rule
        else
            final_value = { tostring(val_rule) }
        end
        local res, matcher = pcall(ipmatcher.new, final_value)
        if res and matcher then
            local ok, _ = matcher:match(ip)
            if ok then
                return true
            end
        else
            return nil, "(op:ip_match) call ipmatcher.new() failed."
        end
        return false
    end,
    _ip_region = function(ip, val_rule)
        local res, err = maxminddb.lookup(ip)
        if not res then
            return nil, "(op:ip_region) failed to LOOKUP ip:[" .. ip .. "]"
        end
        -- res -> table  {"country":{"iso_code":"CN"}}
        if res.country and res.country.iso_code then
            if type(val_rule) == "string"
                and (string.upper(val_rule) == res.country.iso_code)
            then
                return true
            elseif type(val_rule) == "table" then
                for _, v in ipairs(val_rule) do
                    if res.country.iso_code == string.upper(v) then
                        return true
                    end
                end
            end
        end
        return false
    end
}

---获取操作符"取反前缀"字符串
_M.get_not_char = function()
    return not_char
end

---操作符拆分"取反前缀"后的 纯操作符字符串
---@param op string 待拆分操作符
---@return string
_M.split_not_char = function(op)
    local index = string.find(op, not_char, 1, true)
    if not index then
        return op
    end
    local loc = index + string.len(not_char)
    return string.sub(op, loc)
end

---验证操作符是否为合法操作符字串
---@param op string 待验证操作符
---@return boolean
_M.in_allow = function(op)
    local pure_op = _M.split_not_char(op)
    return (op_event["_" .. pure_op] ~= nil)
end

---验证操作符是否合法(须是字符串,不得是空字符串,在允许列表中)
---@param op string 操作符
---@return boolean
---@return string | nil error_info
_M.is_valid = function(op)
    if type(op) ~= "string" then
        return false, "rule:op need string type."
    elseif string.match(op, "^%s*$") then
        return false, "rule:op is empty string."
    elseif not _M.in_allow(op) then
        return false, "rule:op invalid."
    end
    return true
end

---操作执行，返回 true/false。
---     出错返回 nil, 错误信息
---@param operator string 操作符
---@param value_var string 变量1(规则变量的值)
---@param value_rule any 变量2(规则值的值)
---@return boolean | nil
---@return string | nil
_M.execute = function(operator, value_var, value_rule)
    local op_not = is_prefix_not(operator)
    local op
    if op_not then
        op = "_" .. string.lower(_M.split_not_char(operator))
    else
        op = "_" .. string.lower(operator)
    end

    if not op_event[op] then
        return nil, "operator [" .. operator .. "] invalid."
    end
    local result, err = op_event[op](value_var, value_rule)
    if result == nil then
        return result, err
    end
    if op_not then
        return not result
    end
    return result
end


return _M
